/** Mobile base class.'
 */
function Mobile()
{
}
Mobile.prototype = new Entity("Mobile", "@", Color.white, Color.black, true, false);
Mobile.prototype.isMobile = true;
/** Create a new mobile from a named mobile description.
 *@param id The ID of the mobile description in the mobiles data table.
 */
Mobile.newById = function(id)
{
	var ret = new Mobile();
	ret.init.call(ret, mobiles[id]);
	return ret;
}
/** The abbreviations for mod keys.
 */
Mobile.modAbbreviations =
{
	str: "STR",
	dex: "DEX",
	con: "CON",
	intel: "INT",
	hp: "HP",
	mp: "MP",
	speed: "SPD",
	dmg: "DMG",
	rdmg: "RDM",
	ac: "AC"
};
/** Get the string describing the effects of mods.
 *@param mods The modification object.
 */
Mobile.getModEffectString = function(mods)
{
	var o, ret = "";
	for(o in mods)
	{
		if(!mods.hasOwnProperty(o) ||
			!Mobile.modAbbreviations[o] ||
			mods[o] === 0)
			continue;
		if(ret.length > 0)
			ret += " ";
		ret += (mods[o] > 0 ? "%a0" : "%90") + Mobile.modAbbreviations[o] + (mods[o] > 0 ? "+" : "") + mods[o] + "%f0";
	}
	return ret;
};
/** Initialize all of the mobile's variables.
 *
 * Possible property bag keys.
 *@param name The name of this mobile.
 *@param c Character used for this entity.
 *@param fg Foreground color.
 *@param bg Background color.
 *@param str Strength.
 *@param agi Agility.
 *@param end Endurance.
 *@param hp Hit points.
 *@param sp Stamina points.
 *@param level The level of the mobile.
 *@param ac The armor class of the mobile.
 *@param visibilityRange The visibility range of the mobile.
 *@param flags The flags property bag for this mobile.
 *@param controllerDesc Controller description, an object containing fields named for the behaviors defined by Controller.
 */
Mobile.prototype.init = function(bag)
{
	this._skill_mod_athletics = 0;
	this._skill_mod_boxing = 0;
	this._skill_mod_defense = 0;
	this._skill_mod_fighting = 0;
	this._skill_mod_judo = 0;
	this._skill_mod_kungfu = 0;
	this._skill_mod_making = 0;
	this._skill_mod_wrestling = 0;
	this._skill_mod_throwing = 0;
	util.mix(this, bag);
	this.hpMax = this.hp;
	this.spMax = this.sp;
	this.weight = 0;
	this.weightMax = 10 + this.str * 2;
	this.flags = {};
	if(bag.hasOwnProperty("flags"))
	{
		for(var o in bag.flags)
		{
			if(bag.flags.hasOwnProperty(o))
				this.flags[o] = bag.flags[o];
		}
	}
	this.inventory = [];
	this.effects = [];
	this.equipment = [];
	this.weapon = null;
	this.offHandWeapon = null;
	this.controller = new Controller(this, this.controllerDesc);
	this.time = -Math.floor(rand.next(0, 100) - 50);
	this.turnTime = 100;
}
/** Drop an item from the mobile's inventory.
 *@param item The item to drop.
 */
Mobile.prototype.dropItem = function(item)
{
	var idx = this.inventory.indexOf(item);
	if(item.parent != this ||
		idx < 0)
		return;
	if(!this.unequipItem(item))
		return;
	this.removeEntity(item);
	item.x = this.x;
	item.y = this.y;
	this.parent.addEntity(item);
}
/** Unequip an item.
 *@param item The item to equip.
 */
Mobile.prototype.unequipItem = function(item)
{
	// No need to unequip
	if(item === null ||
		item.parent != this)
		return true;
	
	// Unequip weapons
	if(this.weapon === item)
		this.weapon = null;
	else if(this.offHandWeapon === item)
		this.offHandWeapon = null;
	// Unequip equipment
	else
	{
		var idx = this.equipment.indexOf(item);
		if(idx >= 0)
			this.equipment.splice(idx, 1);
		// No need to unequip
		else
			return true;
	}

	// TODO Maybe handle cursed items?

	// Handle mods
	this.removeMods(item.mods);
	
	return true;
}
/** Equip an item.
 *@param item The item to equip.
 */
Mobile.prototype.equipItem = function(item)
{
	// We don't own this item
	if(item.parent !== this)
		return false;
	
	// Try to unequip from current location
	this.unequipItem(item);
	
	// Equip
	if(item.isWeapon())
	{
		if(this.weapon &&
			item.itemType === "1h" &&
			this.weapon.itemType === "1h")
		{
			if(!this.unequipItem(this.offHandWeapon))
				return false;
			this.offHandWeapon = item;
		}
		else
		{
			if(!this.unequipItem(this.weapon))
				return false;
			this.weapon = item;
		}
	}
	else
	{
		this.equipment.push(item);
	}
	
	// Handle mods
	this.applyMods(item.mods);

	return true;
}
/** Test if an item is equiped on the mobile.
 *@param item The item to test.
 */
Mobile.prototype.isEquiped = function(item)
{
	return this.equipment.indexOf(item) >= 0 ||
		this.weapon === item ||
		this.offHandWeapon === item;
}
/** Apply a bag of modifications to the mobile.
 *@param mods The modification property bag.
 */
Mobile.prototype.applyMods = function(mods)
{
	var o;
	if(!mods)
		return;
	for(o in mods)
	{
		if(!mods.hasOwnProperty(o))
			continue;
		if(this[o] === undefined)
			this[o] = 0;
		// Handle SP overflow damage
		if(o === "sp" &&
			mods[o] < 0)
		{
			var dmg = mods[o] * ((100 - this.getProtectionPercent()) / 100);
			if(dmg > -1)
				dmg = -1;
			this.sp += dmg;
			if(this.sp < 0)
			{
				this.hp += this.sp;
				this.sp = 0;
			}
		}
		else
			this[o] += mods[o];
	}
}
/** Remove a bag of modifications to the mobile.
 *@param mods The modification property bag.
 */
Mobile.prototype.removeMods = function(mods)
{
	var o;
	if(!mods)
		return;
	for(o in mods)
	{
		if(!mods.hasOwnProperty(o) ||
			this[o] === undefined)
			continue;
		this[o] -= mods[o];
	}
}
/** Apply an array of flag names to the mobile.
 *@param flags The array of flag names.
 */
Mobile.prototype.applyFlags = function(flags)
{
	for(var i = 0; i < flags.length; ++i)
	{
		this.setFlag(flags[i]);
	}
}
/** Remove an array of flag names from the mobile.
 *@param flags The array of flag names.
 */
Mobile.prototype.removeFlags = function(flags)
{
	for(var i = 0; i < flags.length; ++i)
	{
		this.clearFlag(flags[i]);
	}
}
/** Return the effective speed of the mobile.
 */
Mobile.prototype.getEffectiveSpeed = function()
{
	return 50 + this._skill_athletics;
}
/** Add an effect to this mobile's effect list.
 *@param effect The effect to add.
 */
Mobile.prototype.addEffect = function(effect)
{
	// See if there is another effect with the same __baseId on us, and if
	// there is try to merge this effect with that one.
	for(var i = 0; i < this.effects.length; ++i)
	{
		if(this.effects[i].name == effect.name)
		{
			if(this.effects[i].merge(effect))
				return;
		}
	}
	if(effect.onAttach(this))
		this.effects.push(effect);
}
/** Remove an effect from this mobile's effect list.
 *@param effect The effect to remove.
 */
Mobile.prototype.removeEffect = function(effect)
{
	if(effect.parent !== this)
		return
	if(this.effects.indexOf(effect) < 0)
		return;
	this.effects.splice(this.effects.indexOf(effect), 1);
	effect.onRemove();
}
/** Add an item to this mobile's inventory, or an effect to it's effect list.
 *@param item The item to add.
 */
Mobile.prototype.addEntity = function(item)
{
	if(item.parent)
		item.parent.removeEntity(item);
	item.parent = this;
	this.inventory.push(item);
	this.weight += item.weight;
	this.statCheck();
}
/** Remove an item from this mobile's inventory, or an effect to it's effect list.
 *@param item The item to remove.
 */
Mobile.prototype.removeEntity = function(item)
{
	if(item.parent !== this)
		return
	if(this.inventory.indexOf(item) < 0)
		return;
	this.inventory.splice(this.inventory.indexOf(item), 1);
	this.weight -= item.weight;
	item.parent = null;
}
/** Attacks another mobile.
 *@param target The target of the attack.
 *@returns True if the attack was possible, false if it was not.
 */
Mobile.prototype.attack = function(target)
{
	// Range and LOS check
	if(!this.parent.isVisibleFrom(this.x, this.y, target.x, target.y, 1))
		return false;

	var s = Spell.newById("attack");
	s.apply(this, target);
	target.statCheck();
	this.statCheck();
	return true;
}
/** Attacks another mobile with the mobile's ranged attack.
 *@param target The target of the attack.
 *@returns True if the attack was possible, false if it was not.
 */
Mobile.prototype.rangedAttack = function(target)
{
	// Range and LOS check
	if(!this.parent.isVisibleFrom(this.x, this.y, target.x, target.y, this.range))
		return false;
	
	var s = Spell.newById("ranged attack");
	s.apply(this, target);
	target.statCheck();
	this.statCheck();
	return true;
}
/** Attempts to cast the spell bound to a particular item.
 *
 * This function really just selects the target for the next function,
 * _realCastSpellFromItem.
 *@param item The item to be used.
 */
Mobile.prototype.castSpellFromItem = function(item)
{
	// Validate we can use it
	if(!item.canUse(this, true))
		return;
	
	// Target selection
	var spell = item.getSpell();
	switch(spell.target)
	{
		case "self":
			this._realCastSpellFromItem(item, this);
			break;
		case "ranged":
			dhack.addModeHandler(
				new SelectPositionMode(
					dhack.map,
					this.x,
					this.y,
					spell.range,
					function(x, y, canceled) {
						var ents = dhack.map.getEntitiesAtPosition(x, y);
						for(var i = ents.length - 1; i >= 0; --i)
						{
							if(ents[i].isMobile)
							{
								this._realCastSpellFromItem(item, ents[i]);
								break;
							}
						}
					},
					this
				)
			);
			break;
		default:
			throw new Error("Unhandled target selection type " + spell.target);
	}
}
/** Actually casts a spell from an item after charge and mana validations are
 * done.
 *@param target The target of the spell.
 */
Mobile.prototype._realCastSpellFromItem = function(item, target)
{
	item.use(this, target);
	dhack.map.doTurn();
}
/** Check all stats and statuses (weight, dead, etc).
 */
Mobile.prototype.statCheck = function()
{
	// Overweight checks
	if(this.weight > this.weightMax &&
		!this.flags.encumbered)
	{
		this.addEffect(new Effect({
			name: "Encumbered",
			type: "perm",
			flags: ["encumbered"]
		}));
	}
	if(this.weight <= this.weightMax &&
		this.flags.encumbered)
	{
		for(var i = 0; i < this.effects.length; ++i)
		{
			if(this.effects[i].name === "Encumbered")
			{
				this.removeEffect(this.effects[i]);
				break;
			}
		}
	}
	
	// Max HP and SP enforcement
	if(this.hp > this.hpMax)
		this.hp = this.hpMax;
	if(this.sp > this.spMax)
		this.sp = this.spMax;
	
	// Death check
	if(this.hp <= 0)
		this.die();
}
/** Returns true if the item currently has an equipment penalty.
 *@param item The item to check.
 */
Mobile.prototype.equipPenalty = function(item)
{
	if(item === this.weapon &&
		this.weapon.hasOwnProperty("strReq") &&
		this.weapon.strReq >= this.str)
		return true;
	if(item === this.offHandWeapon &&
		this.offHandWeapon.hasOwnProperty("strReq") &&
		parseInt(this.offHandWeapon.strReq * 1.5) >= this.str)
		return true;
	return false;
}
/** Calculates the damage dealt by this mobile for a normal attack.
 */
Mobile.prototype.getMeleeDamage = function()
{
	var dmg = this.str;
	
	// Weapon bonus
	if(this.weapon)
	{	
		// Strength requirement handling
		if(this.weapon.hasOwnProperty("strReq"))
		{
			if(this.weapon.strReq < this.str)
				dmg += this.weapon.dmg;
			else
				dmg += this.weapon.dmg - (this.weapon.strReq - (this.str - 1)) * 2;
		}
		else
			dmg += this.weapon.dmg;
	}
	// Off-hand weapon bonus
	if(this.offHandWeapon)
	{
		// Strength requirement handling
		if(this.offHandWeapon.hasOwnProperty("strReq"))
		{
			var strReq = parseInt(this.offHandWeapon.strReq * 1.5);
			if(strReq < this.str)
				dmg += this.offHandWeapon.dmg;
			else
				dmg += this.offHandWeapon.dmg - (strReq - (this.str - 1)) * 2;
		}
		else
			dmg += this.offHandWeapon.dmg;
	}
	if(dmg < 1)
		dmg = 1;
	return dmg;
}
/** Calculates the protection value of the worn equipment.
 */
Mobile.prototype.getProtectionPercent = function()
{
	var ac = this.ac;
	if(ac < 0)
		ac = 0;
	if(ac > 100)
		ac = 100;
	return ac;
}
/** Returns the calculated value of a skill after all modifiers.
 *@param skill The lower-cased name of the skill.
 */
Mobile.prototype.getSkillValue = function(skill)
{
	var base = this["_skill_" + skill] + this["_skill_mod_" + skill];
	var bonus = 0;
	switch(skill)
	{
		case "athletics": bonus = (this.str - 10) / 2 + (this.agi - 10) + (this.end - 10) / 2; break;
		case "boxing": bonus = (this.str - 10) * 2; break;
		case "defense": bonus = (this.end - 10) * 2; break;
		case "fighting": bonus = (this.str - 10) + (this.agi - 10); break;
		case "judo": bonus = (this.agi - 10) * 2; break;
		case "kungfu": bonus = (this.str - 10) + (this.agi - 10); break;
		case "making": bonus = (this.str - 10) / 2 + (this.agi - 10) + (this.end - 10) / 2; break;
		case "wrestling": bonus = (this.end - 10) * 2; break;
		case "throwing": bonus = (this.agi - 10) * 2; break;
		default:
			throw new Error("Bad skill name " + skill);
	}
	return base + bonus;
}
/** Called whenever this mobile is the target of a spell.
 */
Mobile.prototype.wasHitBy = function(source)
{
	if(this.controller)
		this.controller.wasHitBy(source);
}
/** Returns the name of the mobile suitable for combat messages.
 *@param capital If true the returned string will have the first letter capitolized.
 */
Mobile.prototype.getMessageName = function(capital)
{
	if(this === dhack.player)
		return capital ? "You" : "you";
	return  (capital ? "The " : "the ") + this.name;
}
/** Death message proceedure.
 */
Mobile.prototype._doDeathMessage = function()
{
	if(this == dhack.player)
		dhack.logWindow.pushMessage("%90You died.");
	else
		dhack.logWindow.pushMessage("%a0The " + this.name + " died.");
}
/** Death proceedure.
 */
Mobile.prototype.die = function()
{
	this._doDeathMessage();
	var corpse = this.getCorpseEntity();
	corpse.x = this.x;
	corpse.y = this.y;
	var parent = this.parent;
	parent.removeEntity(this);
	parent.addEntity(corpse);
}
/** Gets the corpse entity for this mobile.
 */
Mobile.prototype.getCorpseEntity = function()
{
	return new CorpseEntity();
}
/** Do a turn for this mobile.
 */
Mobile.prototype.doTurn = function()
{
	if(this.controller)
		this.controller.doTurn();
	this.endTurn();
}
/** Ends a turn for this mobile.
 */
Mobile.prototype.endTurn = function()
{
	this.consumeTime(this.turnTime);
	this.turnTime = 100;
}
/** Consumes an amount of time for the mobile.
 *@param time The amount of time to consume.
 */
Mobile.prototype.consumeTime = function(time)
{
	this.time -= time;
	this.sp += 1 * (time / 100);
	this.hp += 0.1 * (time/ 100);
	for(var i = 0; i < this.effects.length; ++i)
	{
		this.effects[i].update(time);
	}
	this.statCheck();
}
/** Tries to move the mobile by an offset.
 *@param dx Offset X value
 *@param dy Offset Y value
 */
Mobile.prototype.tryMove = function(dx, dy)
{
	if(dx == 0 && dy == 0)
		return true;
	else if(this.parent.tryMoveEntity(this, this.x + dx, this.y + dy))
		return true;
	return false;
}
/** Sets (increments) a flag.
 * @param name The name of the flag.
 */
Mobile.prototype.setFlag = function(name)
{
	if(!this.flags[name])
		this.flags[name] = 1;
	else
		this.flags[name]++;
}
/** Clears (decrements) a flag.
 * @param name The name of the flag.
 */
Mobile.prototype.clearFlag = function(name)
{
	if(!this.flags[name])
		return;
	this.flags[name]--;
	if(this.flags[name] <= 0)
		delete this.flags[name];
}
ron.registerCtor(Mobile, "Mobile");
